Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 6956f2e19b08a71562cce3dcfb0062777059e031


Parents : b8b40bc
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-06-19T10:49:32-05:00

feat(tests): improve tests for outbound message cancellation and download functionality, including new test cases for canceling in-flight messages and handling downloads in the absence of an Android bridge

Changes
Diff

diff --git a/tests/frontend/ConversationViewer.test.js b/tests/frontend/ConversationViewer.test.js
index bf75ba75..8c61971b 100644
--- a/tests/frontend/ConversationViewer.test.js
+++ b/tests/frontend/ConversationViewer.test.js
@@ -771,6 +771,76 @@ describe("ConversationViewer.vue", () => {
expect(retryButtons).toHaveLength(0);
});
+ it("canCancelOutboundSend is true while outbound message is still sending", () => {
+ const wrapper = mountConversationViewer();
+ const sendingItem = {
+ type: "lxmf_message",
+ is_outbound: true,
+ lxmf_message: {
+ hash: "sending-hash",
+ state: "sending",
+ progress: 12,
+ content: "still going",
+ destination_hash: "test-hash",
+ source_hash: "my-hash",
+ fields: {},
+ },
+ };
+ expect(wrapper.vm.canCancelOutboundSend(sendingItem)).toBe(true);
+ expect(wrapper.vm.canCancelOutboundSend({ ...sendingItem, is_outbound: false })).toBe(false);
+ });
+
+ it("cancelSendingMessage calls cancel API for in-flight outbound hash", async () => {
+ const wrapper = mountConversationViewer();
+ const sendingItem = {
+ type: "lxmf_message",
+ is_outbound: true,
+ is_actions_expanded: true,
+ lxmf_message: {
+ hash: "aa".repeat(16),
+ state: "sending",
+ progress: 40,
+ content: "cancel me",
+ destination_hash: "test-hash",
+ source_hash: "my-hash",
+ fields: {},
+ },
+ };
+ wrapper.vm.chatItems = [sendingItem];
+ const hash = sendingItem.lxmf_message.hash;
+ axiosMock.post.mockResolvedValueOnce({
+ data: { lxmf_message: { ...sendingItem.lxmf_message, state: "cancelled" } },
+ });
+
+ await wrapper.vm.cancelSendingMessage(sendingItem);
+
+ expect(axiosMock.post).toHaveBeenCalledWith(expect.stringContaining(`/lxmf-messages/${hash}/cancel`));
+ expect(sendingItem.is_actions_expanded).toBe(false);
+ expect(wrapper.vm.messageContextMenu.show).toBe(false);
+ });
+
+ it("cancelSendingMessage removes optimistic pending placeholder without API call", async () => {
+ const wrapper = mountConversationViewer();
+ const pendingItem = {
+ type: "lxmf_message",
+ is_outbound: true,
+ lxmf_message: {
+ hash: "pending-abc",
+ state: "sending",
+ content: "queued",
+ destination_hash: "test-hash",
+ source_hash: "my-hash",
+ fields: {},
+ },
+ };
+ wrapper.vm.chatItems = [pendingItem];
+
+ await wrapper.vm.cancelSendingMessage(pendingItem);
+
+ expect(axiosMock.post).not.toHaveBeenCalled();
+ expect(wrapper.vm.chatItems.some((i) => i.lxmf_message?.hash === "pending-abc")).toBe(false);
+ });
+
it("calls retrySendingMessage when retry context menu clicked", async () => {
const wrapper = mountConversationViewer();
const failedChatItem = {

diff --git a/tests/frontend/DownloadUtils.test.js b/tests/frontend/DownloadUtils.test.js
index 3780fa0d..7ab960a8 100644
--- a/tests/frontend/DownloadUtils.test.js
+++ b/tests/frontend/DownloadUtils.test.js
@@ -46,4 +46,22 @@ describe("DownloadUtils", () => {
);
expect(saveDownload).toHaveBeenCalledWith("backup.zip", expect.any(String));
});
+
+ it("downloadFile triggers anchor download when Android bridge is absent", async () => {
+ const click = vi.fn();
+ const append = vi.fn();
+ const remove = vi.fn();
+ const link = { click, remove, download: "", href: "", style: { display: "" } };
+ vi.spyOn(document, "createElement").mockReturnValue(link);
+ vi.spyOn(document.body, "append").mockImplementation(append);
+ URL.createObjectURL = vi.fn(() => "blob:mock");
+
+ await DownloadUtils.downloadFile("browser.bin", new Blob([new Uint8Array([1])]));
+
+ expect(click).toHaveBeenCalledTimes(1);
+ expect(link.download).toBe("browser.bin");
+ expect(link.href).toBe("blob:mock");
+ expect(append).toHaveBeenCalledWith(link);
+ expect(remove).toHaveBeenCalledTimes(1);
+ });
});

diff --git a/tests/frontend/outboundSendQueue.test.js b/tests/frontend/outboundSendQueue.test.js
index 53f0ee66..1ee353c4 100644
--- a/tests/frontend/outboundSendQueue.test.js
+++ b/tests/frontend/outboundSendQueue.test.js
@@ -33,7 +33,7 @@ describe("outboundSendQueue", () => {
expect(maxConcurrent).toBe(1);
});
- it("skips cancelled queued jobs and in-flight job after cancel flag", async () => {
+ it("skips cancelled jobs still waiting in the queue", async () => {
const order = [];
let releaseFirst;
const firstGate = new Promise((r) => {
@@ -44,10 +44,6 @@ describe("outboundSendQueue", () => {
if (job.id === "a") {
await firstGate;
}
- if (job.cancelled) {
- order.push(`skip:${job.id}`);
- return;
- }
order.push(`end:${job.id}`);
});
const q = createOutboundQueue(processJob);
@@ -55,10 +51,27 @@ describe("outboundSendQueue", () => {
q.enqueue({ id: "b", cancelKey: "peer|reply|hello|" });
await new Promise((r) => setTimeout(r, 5));
q.cancelJob({ cancelKey: "peer|reply|hello|" });
- q.cancelJob({ pendingHash: "pending-a" });
releaseFirst();
await new Promise((r) => setTimeout(r, 20));
expect(order).toEqual(["start:a", "end:a"]);
expect(processJob).toHaveBeenCalledTimes(1);
});
+
+ it("sets cancelled on the in-flight job for cooperative abort", async () => {
+ let inFlightJob;
+ let release;
+ const gate = new Promise((r) => {
+ release = r;
+ });
+ const q = createOutboundQueue(async (job) => {
+ inFlightJob = job;
+ await gate;
+ });
+ q.enqueue({ id: "a", pendingHash: "pending-1" });
+ await new Promise((r) => setTimeout(r, 5));
+ q.cancelJob({ pendingHash: "pending-1" });
+ expect(inFlightJob?.cancelled).toBe(true);
+ release();
+ await new Promise((r) => setTimeout(r, 10));
+ });
});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────